﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
using NLog;
using NLog.Config;
using NLog.Targets;
using Timer = System.Timers.Timer;

namespace Ast.Framework.Logging
{
    [Target("FastRichTextBox")]
    public sealed class FastRichTextBoxTarget : TargetWithLayout
    {
        private readonly ILoggingMessageQueue _loggingMessageQueue;

        private int _lineCount;
        private Timer _timer;
        private const int SentInterval = 50;
        private long _lastSentTick = System.Environment.TickCount;

        /// <summary>
        /// Initializes static members of the RichTextBoxTarget class.
        /// </summary>
        /// <remarks>
        /// The default value of the layout is: <code>${longdate}|${level:uppercase=true}|${logger}|${message}</code>
        /// </remarks>
        static FastRichTextBoxTarget()
        {
            var rules = new List<RichTextBoxRowColoringRule>()
            {
                new RichTextBoxRowColoringRule("level == LogLevel.Fatal", "White", "Red", FontStyle.Bold),
                new RichTextBoxRowColoringRule("level == LogLevel.Error", "Red", "Empty", FontStyle.Bold | FontStyle.Italic),
                new RichTextBoxRowColoringRule("level == LogLevel.Warn", "Orange", "Empty", FontStyle.Underline),
                new RichTextBoxRowColoringRule("level == LogLevel.Info", "Black", "Empty"),
                new RichTextBoxRowColoringRule("level == LogLevel.Debug", "Gray", "Empty"),
                new RichTextBoxRowColoringRule("level == LogLevel.Trace", "DarkGray", "Empty", FontStyle.Italic),
            };

            DefaultRowColoringRules = rules.AsReadOnly();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="FastRichTextBoxTarget" /> class.
        /// </summary>
        /// <remarks>
        /// The default value of the layout is: <code>${longdate}|${level:uppercase=true}|${logger}|${message}</code>
        /// </remarks>
        public FastRichTextBoxTarget(ILoggingMessageQueue loggingMessageQueue)
        {
            _loggingMessageQueue = loggingMessageQueue;

            this.WordColoringRules = new List<RichTextBoxWordColoringRule>();
            this.RowColoringRules = new List<RichTextBoxRowColoringRule>();
        }

        /// <summary>
        /// Gets the default set of row coloring rules which applies when <see cref="UseDefaultRowColoringRules"/> is set to true.
        /// </summary>
        public static ReadOnlyCollection<RichTextBoxRowColoringRule> DefaultRowColoringRules { get; private set; }

        /// <summary>
        /// Gets or sets the Name of RichTextBox to which Nlog will write.
        /// </summary>
        /// <docgen category='Form Options' order='10' />
        [RequiredParameter]
        public string ControlName { get; set; }

        /// <summary>
        /// Gets or sets the name of the Form on which the control is located. 
        /// If there is no open form of a specified name than NLog will create a new one.
        /// </summary>
        /// <docgen category='Form Options' order='10' />
        [RequiredParameter]
        public string FormName { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether to use default coloring rules.
        /// </summary>
        /// <docgen category='Highlighting Options' order='10' />
        [DefaultValue(false)]
        public bool UseDefaultRowColoringRules { get; set; }

        /// <summary>
        /// Gets the row coloring rules.
        /// </summary>
        /// <docgen category='Highlighting Options' order='10' />
        [ArrayParameter(typeof (RichTextBoxRowColoringRule), "row-coloring")]
        public IList<RichTextBoxRowColoringRule> RowColoringRules { get; private set; }

        /// <summary>
        /// Gets the word highlighting rules.
        /// </summary>
        /// <docgen category='Highlighting Options' order='10' />
        [ArrayParameter(typeof (RichTextBoxWordColoringRule), "word-coloring")]
        public IList<RichTextBoxWordColoringRule> WordColoringRules { get; private set; }

        /// <summary>
        /// Gets or sets a value indicating whether scroll bar will be moved automatically to show most recent log entries.
        /// </summary>
        /// <docgen category='Form Options' order='10' />
        public bool AutoScroll { get; set; }

        /// <summary>
        /// Gets or sets the maximum number of lines the rich text box will store (or 0 to disable this feature).
        /// </summary>
        /// <remarks>
        /// After exceeding the maximum number, first line will be deleted. 
        /// </remarks>
        /// <docgen category='Form Options' order='10' />
        public int MaxLines { get; set; }

        /// <summary>
        /// Gets or sets the form to log to.
        /// </summary>
        internal Form TargetForm { get; set; }

        /// <summary>
        /// Gets or sets the rich text box to log to.
        /// </summary>
        internal RichTextBox TargetRichTextBox { get; set; }

        /// <summary>
        /// Initializes the target. Can be used by inheriting classes
        /// to initialize logging.
        /// </summary>
        protected override void InitializeTarget()
        {
            var openFormByName = Application.OpenForms[this.FormName];
            if (openFormByName == null)
            {
                throw new NLogConfigurationException("Rich text box form '" + this.FormName + "' cannot be found.");
            }

            this.TargetForm = openFormByName;
            if (string.IsNullOrEmpty(this.ControlName))
            {
                throw new NLogConfigurationException("Rich text box control name must be specified for " + this.GetType().Name + ".");
            }

            this.TargetRichTextBox = this.TargetForm.Controls.Find(this.ControlName, true).FirstOrDefault() as RichTextBox;
            if (this.TargetRichTextBox == null)
            {
                throw new NLogConfigurationException("Rich text box control '" + this.ControlName + "' cannot be found on form '" + this.FormName + "'.");
            }

            _timer = new Timer {Interval = SentInterval};
            _timer.Elapsed += (sender, args) =>
            {
                if (!Monitor.TryEnter(_timer)) return;

                try
                {
                    if (_loggingMessageQueue.IsEmpty() || this.TargetRichTextBox.Disposing || this.TargetRichTextBox.IsDisposed) return;

                    UpdateLastSentTick();
                    this.TargetRichTextBox.Invoke(new Action(() =>
                    {
                        var count = 0;
                        LogEventInfo logEventInfo;
                        while ((logEventInfo = _loggingMessageQueue.Dequeue()) != null)
                        {
                            var logMessage = Layout.Render(logEventInfo);
                            var matchingRule = GetMatchingRule(logEventInfo);
                            SendTheMessageToRichTextBox(logMessage, matchingRule);
                            if (++count > 10) break; // allow UI response
                        }
                    }));
                }
                finally
                {
                    Monitor.Exit(_timer);
                }
            };
            _timer.Start();
        }

        /// <summary>
        /// Log message to RichTextBox.
        /// </summary>
        /// <param name="logEvent">The logging event.</param>
        protected override void Write(LogEventInfo logEvent)
        {
            if (TargetRichTextBox.Disposing || TargetRichTextBox.IsDisposed) return;

            if (_loggingMessageQueue.IsEmpty() && CanSendDirectly())
            {
                // send directly
                var logMessage = Layout.Render(logEvent);
                var matchingRule = GetMatchingRule(logEvent);
                TargetRichTextBox.BeginInvoke(new Action(() => SendTheMessageToRichTextBox(logMessage, matchingRule)));
                UpdateLastSentTick();
            }
            else
            {
                // put in message queue
                _loggingMessageQueue.Enqueue(logEvent);
            }
        }

        private RichTextBoxRowColoringRule GetMatchingRule(LogEventInfo logEvent)
        {
            var matchingRule = RowColoringRules.FirstOrDefault(rule => rule.CheckCondition(logEvent));

            if (UseDefaultRowColoringRules && matchingRule == null)
            {
                matchingRule = DefaultRowColoringRules.FirstOrDefault(rule => rule.CheckCondition(logEvent));
            }

            return matchingRule ?? RichTextBoxRowColoringRule.Default;
        }

        private static Color GetColorFromString(string color, Color defaultColor)
        {
            if (color == "Empty")
            {
                return defaultColor;
            }

            return Color.FromName(color);
        }

        private void SendTheMessageToRichTextBox(string logMessage, RichTextBoxRowColoringRule rule)
        {
            RichTextBox rtbx = this.TargetRichTextBox;

            int startIndex = rtbx.Text.Length;
            rtbx.SelectionStart = startIndex;
            rtbx.SelectionBackColor = GetColorFromString(rule.BackgroundColor, rtbx.BackColor);
            rtbx.SelectionColor = GetColorFromString(rule.FontColor, rtbx.ForeColor);
            rtbx.SelectionFont = new Font(rtbx.SelectionFont, rtbx.SelectionFont.Style ^ rule.Style);
            rtbx.AppendText(logMessage + "\n");
            rtbx.SelectionLength = rtbx.Text.Length - rtbx.SelectionStart;

            // find word to color
            foreach (RichTextBoxWordColoringRule wordRule in this.WordColoringRules)
            {
                MatchCollection mc = wordRule.CompiledRegex.Matches(rtbx.Text, startIndex);
                foreach (Match m in mc)
                {
                    rtbx.SelectionStart = m.Index;
                    rtbx.SelectionLength = m.Length;
                    rtbx.SelectionBackColor = GetColorFromString(wordRule.BackgroundColor, rtbx.BackColor);
                    rtbx.SelectionColor = GetColorFromString(wordRule.FontColor, rtbx.ForeColor);
                    rtbx.SelectionFont = new Font(rtbx.SelectionFont, rtbx.SelectionFont.Style ^ wordRule.Style);
                }
            }

            if (this.MaxLines > 0)
            {
                this._lineCount++;
                if (this._lineCount > this.MaxLines)
                {
                    int pos = rtbx.GetFirstCharIndexFromLine(1);
                    rtbx.Select(0, pos);
                    rtbx.SelectedText = string.Empty;
                    this._lineCount--;
                }
            }

            if (this.AutoScroll)
            {
                rtbx.Select(rtbx.TextLength, 0);
                rtbx.ScrollToCaret();
            }
        }

        protected override void CloseTarget()
        {
            if (_timer != null)
            {
                _timer.Close();
            }
        }

        public void CleanLogs()
        {
            TargetRichTextBox.BeginInvoke(new Action(() =>
            {
                TargetRichTextBox.Clear();
                _lineCount = 0;
                _loggingMessageQueue.Clear();
            }));
        }

        private void UpdateLastSentTick()
        {
            _lastSentTick = System.Environment.TickCount;
        }

        private bool CanSendDirectly()
        {
            return System.Environment.TickCount - _lastSentTick > SentInterval;
        }

        public class LogItem
        {
            public string Message { get; set; }
            public LogEventInfo LogEvent { get; set; }
        }
    }
}